/*
 * Copyright (c) 2006 Israfil Consulting Services Corporation
 * Copyright (c) 2006 Christian Edward Gruber
 * All Rights Reserved
 * 
 * This software is licensed under the Berkeley Standard Distribution license,
 * (BSD license), as defined below:
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this 
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of Israfil Consulting Services nor the names of its contributors 
 *    may be used to endorse or promote products derived from this software without 
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 
 * OF SUCH DAMAGE.
 * 
 * $Id: AbstractFlexMojo.java 575 2007-12-20 15:25:40Z christianedwardgruber $
 */
package net.israfil.mojo.flex2;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;

/**
 * Base class for flex2/actionscript3 plugin
 *
 * @author <a href="cgruber@israfil.net">Christian Edward Gruber</a>
 * @author <a href="tspurway@gmail.com">Tim Spurway</a>
 * @version $Id: AbstractFlexMojo.java 575 2007-12-20 15:25:40Z christianedwardgruber $
 */
public abstract class AbstractFlexMojo extends AbstractMojo {
	

	/**
	 * The directory in which Flex (command-line or builder) is installed.
	 * 
	 * @parameter expression="${flex.home}"
	 * @required 
	 * 
	 */
	protected File flexHome;

	/**
	 * Resolve extra libraries (such as charts or fds) from 
	 * ${flex.home}/frameworks/libs.  Default is false, in which case 
	 * these (and other) swcs should be deployed as flex artifacts (swcs).
	 * 
	 * @parameter alias="resolve-extra-framework-libs" default="false"
	 */
	protected boolean resolveExtraSwcsFromFlexFrameworksLibs = false;

	/**
	 * The location of the flex configuration file.  Uses the installed
	 * default flex-config.xml file if none supplied.
	 * 
	 * @parameter expression="${flex.config}" default-value="${flex.home}/frameworks/flex-config.xml"
	 * @required 
	 * 
	 */
	protected File flexConfig;

	/**
	 * The directory into which to place the resulting artifact.  note that this is a read-only 
	 * parameter, satisfied from the build directory.  Attempts to configure this should use the 
	 * standard &lt;build&gt;&lt;directory/&gt;&lt;/build&gt; approach.
	 * 
	 * @parameter expression="${project.build.directory}"
	 * @required
	 * @readonly
	 */
	protected File outputDirectory;

    /**
     * The source directories containing the sources to be compiled.
     *
     * @parameter expression="${project.compileSourceRoots}"
     * @required
     * @readonly
     */
    protected List compileSourceRoots;

    
    /**
     * Location of the flex sources.
     *
     * @parameter expression="${flex.compiler.source}" default="src/main/flex"
     */
    protected File source;

    /**
	 * Name of the generated compiled binary file.
	 * 
	 * @parameter expression="${project.build.finalName}"
	 * @required
	 */
	protected String finalName;
   
    /**
	 * The current locale.
	 * 
	 * @parameter expression="${flex.locale}" default="en_US"
	 */
	protected String locale = "en_US";

    /**
     * @parameter expression="${flex.compiler.optimize}" default="false"
     */
    protected boolean optimize;

    /**
     * @parameter expression="${flex.compiler.profile}" default="false"
     */
    protected boolean profile;

    /**
     * @parameter expression="${flex.compiler.strict}" default="false"
     */
    protected boolean strict;

    /**
     * @parameter expression="${flex.compiler.use-network}" default="false"
     */
    protected boolean useNetwork;
	
    /**
     * @parameter expression="${flex.compiler.show-warnings}" default="false"
     */
    protected boolean warnings;

    /**
     * @parameter expression="${flex.compiler.incremental}" default="false"
     */
    protected boolean incremental;

    /**
     * @parameter expression="${flex.compiler.show-actionscript-warnings}" default="false"
     */
    protected boolean showActionscriptWarnings;

    /**
     * @parameter expression="${flex.compiler.show-binding-warnings}" default="false"
     */
    protected boolean showBindingWarnings;

    /**
     * @parameter expression="${flex.compiler.show-deprecation-warnings}" default="false"
     */
    protected boolean showDeprecationWarnings;
	

    /**
     * @parameter expression="${flex.licenses}" 
     */
    protected License[] licenses;
	

    /**
     * @parameter expression="${flex.extraParameters}" 
     */
    protected Parameter[] extraParameters;
    
    /**
     * @parameter expression="${flex.dataservices.config}" 
     */
    protected File dataServicesConfig;
   
    /**
     * These are additional JVM options used when invoking the Flex compiler
     *
     * @parameter expression="${flex.java.options}"
     */
    protected String[] javaOpts;

	/**
	 * Classifier to add to the artifact generated. If given, the artifact will
	 * be an attachment instead.
	 * 
	 * @parameter
	 */
	protected String classifier;

    /**
     * The maven project.
     *
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;
    
    /**
     * @parameter expression="${plugin.artifacts}"
     * @required
     * @readonly
     */
    protected List pluginArtifacts;
    
    /**
     * @component
     */
    protected MavenProjectHelper projectHelper;

    
	public AbstractFlexMojo() {
		super();
	}

	/**
     * Overload this to produce a test-jar, for example.
     */
    protected String getClassifier() {
    	return null;
    }

    protected abstract String getCompilerClass();    

    protected File getOutputDirectory() {
    	return outputDirectory;
	}

    protected abstract String getFileExtension();
    
    protected File getFile( File basedir, String finalName, String classifier ) {
        if ( classifier != null && !classifier.equals("") ) {
        	if ( classifier.trim().length() > 0 && !classifier.startsWith( "-" )){
        		classifier = "-" + classifier;
        	}
            return new File( basedir, finalName + "-" + getClassifier() + ".jar" );
        } else {
        	return new File( basedir, finalName + "." + getFileExtension() );
        }
    }
    
    /**
     * Override and implement this method to take the existing java parameters
     * and add any final information requried to provide source files to the
     * compiler.  
     * @param parameterList
     */
    protected abstract void finalizeParameters(List parameterList) throws MojoExecutionException, MojoFailureException;
    
    protected final MavenProject getProject() {
        return project;
    }	
    
    protected void addSourcePath(List parameterList) {
    	Iterator i = compileSourceRoots.iterator();
    	if (i.hasNext());
    	parameterList.add( "-source-path" );
    	while (i.hasNext()) {
    		String path = (String)i.next();
			if (new File(path).exists()) {
	    		getLog().debug("Adding source path: " + path);
		    	parameterList.add( path );
			} else {
	    		getLog().debug("Ignoring missing source path: " + path);
			}
    	}
    }
     
	public void execute() throws MojoExecutionException, MojoFailureException {
		
		if (!flexHome.exists())
			throw new MojoExecutionException(flexHome + " does not exist.  flex.home property must be set.");

		Set includedLibraryPaths = new HashSet();
		Set linkedLibraryPaths = new HashSet();

		this.getLog().debug("Creating output directory: " + getOutputDirectory());
		outputDirectory.mkdirs();
		
		
		// HACK: why is this not being set in the defaults???
		if (source == null) source = new File(project.getBasedir(),"src/main/flex");
		if ( !source.exists() ) 				
			throw new MojoExecutionException("Source directory " + source + " does not exist.");
		else
			this.getLog().info("Compiling Flex 2 code");

		
		//
		// Setup the Command-line of java execution.
		//
		List commandLineArgs = new ArrayList();
        
        if( javaOpts != null )
            for( int i = 0; i < javaOpts.length; i++ )
                commandLineArgs.add( javaOpts[i] );
		commandLineArgs.add("-classpath");
		StringBuffer compilerClasspath = new StringBuffer();
		try {
			File flexJars = new File(flexHome,"lib");
			if (!flexJars.exists())
				throw new MojoExecutionException("Flex lib not found.");
			File[] libs = flexJars.listFiles();
			// assume libs is not null and get all .jars in the lib folder;
			for (int i = 0 ; i < libs.length ; i++) {
				File lib = libs[i];
				if (lib.exists()) {
					String path = lib.getCanonicalFile().getAbsolutePath();
					if (path.endsWith(".jar")) {
						addClasspathEntry(compilerClasspath, path);
					}
				} else getLog().debug("Ignoring non-existant lib: " + lib);
			}
			
			// find this plugin's artifact and add the .jar to the classpath.
			
			for (Iterator i = pluginArtifacts.iterator(); i.hasNext() ;) {
				Artifact artifact = (Artifact)i.next();
				getLog().debug("Trying artifact to add to classpath: " + artifact);
				if (artifact.getGroupId().equals("net.israfil.mojo") && artifact.getArtifactId().equals("maven-flex2-plugin-support")) {
					addClasspathEntry(compilerClasspath, artifact.getFile().getCanonicalFile().getAbsolutePath());
					break;
				}
			}
			
			//MojoDescriptor mojoDescriptor = (MojoDescriptor)pluginMojos.get(0);
			//PluginDescriptor pluginDescriptor = mojoDescriptor.getPluginDescriptor();
			//Artifact thisPlugin = (Artifact)pluginDescriptor.getArtifactMap().get("net.israfil.mojo:maven-flex2-plugin");
			// add this plugin to the classpath to allow the wrapper to be present.
			//Artifact thisPlugin = (Artifact)project.getPluginArtifactMap().get("net.israfil.mojo:maven-flex2-plugin");
			
			
			if (getLog().isDebugEnabled())
				getLog().debug("Using classpath: " + compilerClasspath.toString());

		} catch (MalformedURLException e) { 
			throw new MojoExecutionException("Could not generate a URL for library.",e); 
		} catch (IOException e) { 
			throw new MojoExecutionException("Could not generate a canonical path for library.",e); 
		}
		commandLineArgs.add(compilerClasspath.toString());
	
		//
		// Setup the Java parameters.
		//
		
		List javaParms = new ArrayList();

    	// define output file.
		String classifier = getClassifier();
		File outFile = getFile(outputDirectory, finalName, classifier);
		javaParms.add("-output");
		try {
			javaParms.add(outFile.getCanonicalFile().getAbsolutePath());
		} catch (IOException e) {
			throw new MojoExecutionException("Exception attempting to set output file: " + outFile, e);
		}
		
		javaParms.add("-load-config");
		try {
			javaParms.add(flexConfig.getCanonicalFile().getAbsolutePath());
		} catch (IOException e) {
			throw new MojoExecutionException("Exception attempting to set output file: " + outFile, e);
		}
		
		// add in locale
		javaParms.add("-compiler.locale");
		javaParms.add(locale);
		
		// add in license
		
		getLog().info("Attaching licenses.");
		for (int i = 0; licenses != null && i < licenses.length; i++) {
			getLog().debug("Adding license: " + licenses[i].getProduct() + "=" + licenses[i].getSerialNumber());
			javaParms.add("-licenses.license");
			javaParms.add(licenses[i].getProduct());
			javaParms.add(licenses[i].getSerialNumber());
		}

		// add in binary options.
		if (optimize) javaParms.add("-compiler.optimize");
		if (profile) javaParms.add("-compiler.profile");
		if (strict) javaParms.add("-compiler.strict");
		if (useNetwork) javaParms.add("-use-network");
		if (warnings) javaParms.add("-warnings");
		if (incremental) javaParms.add("-compiler.incremental");
		if (showActionscriptWarnings) javaParms.add("-show-actionscript-warnings");
		if (showBindingWarnings) javaParms.add("-show-binding-warnings");
		if (showDeprecationWarnings) javaParms.add("-show-deprecation-warnings");
		//verbose-stacktraces
		
		if (dataServicesConfig != null) {
			javaParms.add("-compiler.services");
			try {
				javaParms.add(dataServicesConfig.getCanonicalFile().getAbsolutePath());
			} catch (IOException e) {
				throw new MojoExecutionException("Exception attempting to set output file: " + outFile, e);
			}			
		}
		File fwLibDir = new File(flexHome,"frameworks/libs");
		try {
			linkedLibraryPaths.add(new File(fwLibDir,"framework.swc").getCanonicalFile().getAbsolutePath());
			linkedLibraryPaths.add(new File(fwLibDir,"playerglobal.swc").getCanonicalFile().getAbsolutePath());
			linkedLibraryPaths.add(new File(fwLibDir,"flex.swc").getCanonicalFile().getAbsolutePath());
			linkedLibraryPaths.add(new File(fwLibDir,"utilities.swc").getCanonicalFile().getAbsolutePath());
			linkedLibraryPaths.add(new File(fwLibDir,"rpc.swc").getCanonicalFile().getAbsolutePath());
			if (resolveExtraSwcsFromFlexFrameworksLibs) {
				if (new File(fwLibDir,"charts.swc").exists())
					//javaParms.add("-include-libraries+=" + new File(fwLibDir,"charts.swc").getCanonicalFile().getAbsolutePath());
					linkedLibraryPaths.add(new File(fwLibDir,"charts.swc").getCanonicalFile().getAbsolutePath());
				if (new File(fwLibDir,"fds.swc").exists())
					linkedLibraryPaths.add(new File(fwLibDir,"fds.swc").getCanonicalFile().getAbsolutePath());
			}
		} catch (IOException e) {
			throw new MojoExecutionException("IOException trying to add to included libraries.",e);
		}


		//	external-library-path path-element [...]   	
		//	     Specifies a list of SWC files or directories to exclude from linking when compiling a SWF file. This option provides compile-time link checking for external components that are dynamically linked.
		//	     For more information about dynamic linking, see About linking.
		//		 You can use the += operator to append the new SWC file to the list of external libraries.

		// runtime-shared-libraries (deployed runtime lib location - 1 for each)
		
		
		// Process 
		Set artifacts = project.getArtifacts();
		getLog().debug("Dependency artifacts: " + artifacts);
		Iterator depsIterator = artifacts.iterator();
		while (depsIterator.hasNext()) {
			Artifact dep = (Artifact)depsIterator.next();
	        // TODO: let the scope handler deal with this
	        if ( Artifact.SCOPE_COMPILE.equals( dep.getScope() ) || 
				        	Artifact.SCOPE_PROVIDED.equals( dep.getScope() ) ||
				            Artifact.SCOPE_SYSTEM.equals( dep.getScope() ) ) {
				if ("swc".equals(dep.getType())) {
					getLog().debug("Linking dependency: " + dep);
					try {
						String absPath = dep.getFile().getCanonicalFile().getAbsolutePath();
						linkedLibraryPaths.add(absPath);
						//includedLibraryPaths.add(absPath);
					} catch (IOException e) {
						throw new MojoExecutionException("Dependency file not present or readable: " + dep.getFile().getAbsolutePath(),e);
					}
				} else if ("swf".equals(dep.getType())) {
					throw new MojoExecutionException("Cannot link swf project '" + 
						dep + "' into other swf.  Compile dependencies must be .swcs.");
				}
	        } else {
	        	getLog().debug("Skipping non-compile artifact: " + dep);
	        }
	        
		}

		
		
		
		// add included and linked library paths.
		Iterator inclLibsIter = includedLibraryPaths.iterator();
		while (inclLibsIter.hasNext()) 
			javaParms.add("-include-libraries+=" + inclLibsIter.next());

		
		
		
		/*
			-benchmark
			-compiler.accessible
			-compiler.actionscript-file-encoding <string>
			-compiler.context-root <context-path>
			-compiler.fonts.max-glyphs-per-face <string>
			-compiler.include-libraries [library] [...]

			-compiler.namespaces.namespace <uri> <manifest>
			-compiler.source-path [path-element] [...]
			-compiler.theme [filename] [...]
			-compiler.use-resource-bundle-metadata
			-file-specs [path-element] [...]
			-help [keyword] [...]
			-metadata.contributor <name>
			-metadata.creator <name>
			-metadata.date <text>
			-metadata.description <text>
			-metadata.language <code>
			-metadata.localized-description <text> <lang>
			-metadata.localized-title <title> <lang>
			-metadata.publisher <name>
			-metadata.title <text>
			-runtime-shared-libraries [url] [...]
		 */
		
		// hack in the source, since we're not using a proper plexus compiler yet.
    	compileSourceRoots.add(new File(project.getBasedir(),"src/main/flex").getPath());

    	finalizeParameters(javaParms);

    	getLog().info("Adding Extra Parameters: " + Arrays.asList(extraParameters));
		Iterator extraParmsIter = Arrays.asList(extraParameters).iterator();
		while (extraParmsIter.hasNext()) {
			Parameter p = (Parameter)extraParmsIter.next();
			getLog().debug("Adding parameter " + p.getName());
			javaParms.add("-" + p.getName());
			if (p.getValues() != null) {
				Iterator values = Arrays.asList(p.getValues()).iterator();
				while (values.hasNext()) {
					String value = (String)values.next();
					getLog().debug("Adding parameter value" + value);
					javaParms.add(value);					
				}
			}
		}
    	
    	int result = -1;

		try {
			source = source.getCanonicalFile(); // try to reduce size of line.
		} catch (IOException e) {}

        try {
            result = executeJavaCommand("java", source.getAbsolutePath(), commandLineArgs, getCompilerClass(), javaParms);
            if ( result != 0 ) 
                throw new MojoExecutionException( "Result of " + getCompilerClass() + " execution is: '" + result + "'." );
        } catch ( CommandLineException e ) {
            throw new MojoExecutionException( "Command execution failed.", e );
        }
		
		// Point Maven2 at the outbound file.
		if (classifier != null) {
			projectHelper.attachArtifact(getProject(), "", classifier, outFile);
		} else {
			getProject().getArtifact().setFile(outFile);
		}
	}
	
	protected void addClasspathEntry(StringBuffer compilerClasspath, String path) {
		getLog().debug("Adding " + path + " to path.");
		if (compilerClasspath.length() != 0) {
			compilerClasspath.append(System.getProperty("path.separator"));
		} 
		compilerClasspath.append(path);
	}
	
	protected int executeJavaCommand(String cmd, String cwd, List commandLineArgs, String className, List programArgs) throws CommandLineException {
		
    	// Attempt to invoke the compiler
		// create the array for the method sig consisting of one
		// string array representing command-line params.
		getLog().debug("Invoking " + className + " with parameters: " + programArgs);
		
        Commandline cl = new Commandline();
        cl.setExecutable( cmd );
        Iterator clargsIterator = commandLineArgs.iterator();
        while ( clargsIterator.hasNext() ) 
        	cl.createArgument().setValue( (String)clargsIterator.next() );

        cl.createArgument().setValue( StreamedParameterExecutableWrapper.class.getCanonicalName() );
        
        cl.createArgument().setValue( className );
        
        InputStream argIs = StringStreamUtil.prepareInputStreamFromStrings(programArgs);
        
        cl.setWorkingDirectory( cwd );
        
        StreamConsumer consumer = new StreamConsumer() {
            public void consumeLine( String line ) { getLog().info( line ); }
        };
        
        getLog().debug("Command line: " + cl);	
        
        return CommandLineUtils.executeCommandLine( cl, argIs, consumer, consumer );
        
	}
	

}
